/* ************************************************************************
 * @file    mmp_drv.c
 * @brief   一个简单的Linux字符设备驱动程序，用于模拟LCD帧缓冲。
 * 该驱动程序提供了一块可供用户空间mmap的内存区域，模拟LCD的显存，
 * 并支持ioctl命令进行简单的控制和数据交换。
 *
 * @version 1.0
 * @date    2019年11月10日 02时29分17秒 PST
 * @author  tiydy,
 * @company None
 ************************************************************************/

#include <linux/init.h>         // 模块初始化和退出宏定义
#include <linux/module.h>       // 模块相关定义
#include <linux/fs.h>           // 文件系统相关结构体和函数 (file_operations, inode等)
#include <linux/device.h>       // 设备模型相关定义 (class_create, device_create等)
#include <linux/interrupt.h>    // 中断处理相关定义 (此处未使用)
#include <linux/gpio.h>         // GPIO相关定义 (此处未使用)
#include <linux/input.h>        // 输入子系统相关定义 (此处未使用)
#include <linux/wait.h>         // 等待队列相关定义 (此处未使用)
#include <linux/sched.h>        // 进程调度相关定义 (此处未使用)
#include <linux/poll.h>         // poll机制相关定义 (此处未使用)
#include <linux/slab.h>         // 内核内存分配 (kzalloc, kfree)
#include <linux/fb.h>           // 帧缓冲设备相关定义 (struct fb_fix_screeninfo, struct fb_var_screeninfo)
#include <linux/mm.h>           // 内存管理相关定义 (mmap, vm_area_struct, virt_to_phys, io_remap_pfn_range)
#include <asm/uaccess.h>        // 用户空间访问函数 (copy_to_user, copy_from_user, access_ok, __put_user, __get_user)
#include <asm/device.h>         // 设备模型相关定义 (已包含在 <linux/device.h> 中，可移除)
#include <linux/cdev.h>         // 字符设备相关定义 (cdev_alloc, cdev_init, cdev_add, cdev_del)

/**
 * @brief 模拟的一块内核虚拟内存，用于作为LCD的帧缓冲。
 * 大小为PAGE_SIZE (800*480*4字节)。
 */
void *virt_mem;

/**
 * @brief 当前写入内存的偏移量。
 * 用于记录`write`操作的当前位置。
 */
unsigned long pos = 0;

/* 定义ioctl幻数和命令 */
#define MEMDEV_IOC_MAGIC 'k'             ///< 定义ioctl幻数
#define MEMDEV_IOCPRINT _IO(MEMDEV_IOC_MAGIC, 1) ///< 打印设备信息命令 (无参数)
#define MEMDEV_IOCGETDATA _IOR(MEMDEV_IOC_MAGIC, 2, int) ///< 获取数据命令 (从内核读一个int)
#define MEMDEV_IOCSETDATA _IOW(MEMDEV_IOC_MAGIC, 3, int) ///< 设置数据命令 (向内核写一个int)
#define MEMDEV_IOC_MAXNR 3               ///< 最大ioctl命令编号

#define DEVICENAME "ubuntu_lcd" ///< 定义设备名称
#define PAGE_SIZE (800 * 480 * 4) ///< 定义模拟LCD的页面大小 (800x480像素，32位色深)

/**
 * @brief LCD固定屏幕信息结构体。
 * 用于存储LCD的固定参数，如行长度、类型等。
 */
struct fb_fix_screeninfo finfo;

/**
 * @brief LCD可变屏幕信息结构体。
 * 用于存储LCD的可变参数，如分辨率、位深等。
 */
struct fb_var_screeninfo vinfo;

/**
 * @brief LCD屏幕占用的字节空间大小。
 * 根据vinfo中的分辨率和位深计算。
 */
unsigned int page_size2 = 0;

/**
 * @brief 字符设备结构体指针。
 * 用于管理字符设备的注册和操作。
 */
struct cdev *cdev_t;

/**
 * @brief 设备号，包含主设备号和次设备号。
 * 通过alloc_chrdev_region动态分配。
 */
dev_t dev = 0;

/**
 * @brief 设备类结构体指针。
 * 用于在/sys/class目录下创建设备类，方便udev自动创建设备节点。
 */
static struct class *cdev_class;

/**
 * @brief 内部缓冲区，用于`read`操作返回固定字符串。
 */
static char bufrh[1024] = "hello drv test!";

/**
 * @brief 设备打开回调函数。
 *
 * @param inodep inode指针，代表文件节点。
 * @param filep file指针，代表打开的文件实例。
 * @return 成功返回0。
 */
static int ubuntu_lcd_open(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "ubuntu_lcd: device opened\n");
    pos = 0; // 每次打开时重置写入位置
    return 0;
}

/**
 * @brief 设备关闭回调函数。
 *
 * @param inodep inode指针。
 * @param filep file指针。
 * @return 成功返回0。
 */
static int ubuntu_lcd_release(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "ubuntu_lcd: device closed\n");
    return 0;
}

/**
 * @brief 设备读取回调函数。
 * 从驱动读取固定字符串`bufrh`到用户空间。
 *
 * @param filep file指针。
 * @param buf 用户空间缓冲区指针，用于存放读取到的数据。
 * @param count 期望读取的字节数 (此处未完全使用，总是返回固定字符串长度)。
 * @param offset 文件偏移量指针 (此处未使用)。
 * @return 成功读取的字节数 (固定字符串长度)，失败返回0。
 */
static ssize_t ubuntu_lcd_read(struct file *filep, char __user *buf, size_t count, loff_t *offset)
{
    // 将内核空间的bufrh复制到用户空间的buf
    if (copy_to_user(buf, bufrh, strlen(bufrh)))
    {
        printk(KERN_ERR "ubuntu_lcd: copy_to_user fail!\n");
        return 0; // 复制失败返回0
    }
    return strlen(bufrh); // 返回实际读取的字节数
}

/**
 * @brief 设备写入回调函数。
 * 从用户空间写入数据到模拟的帧缓冲`virt_mem`。
 * 数据会追加到`pos`指示的位置。
 *
 * @param filep file指针。
 * @param buf 用户空间缓冲区指针，包含要写入的数据。
 * @param count 期望写入的字节数。
 * @param offse 文件偏移量指针 (此处未使用)。
 * @return 成功写入的字节数，失败返回0。
 */
static ssize_t ubuntu_lcd_write(struct file *filep, const char __user *buf, size_t count, loff_t *offse)
{
    unsigned char *ptr = (unsigned char *)virt_mem;

    // 检查写入位置是否超出模拟内存的范围
    if (pos >= page_size2) // 使用page_size2作为总大小限制
    {
        printk(KERN_WARNING "ubuntu_lcd: write position exceeds memory limit.\n");
        return 0; // 超出范围，不写入
    }

    // 将用户空间的buf数据复制到内核空间的virt_mem
    // 确保写入的数据不会超出virt_mem的边界
    if (copy_from_user(ptr + pos, buf, min((size_t)(page_size2 - pos), count)))
    {
        printk(KERN_ERR "ubuntu_lcd: copy_from_user fail!\n");
        return 0; // 复制失败返回0
    }
    else
    {
        pos += count; // 更新写入位置
    }

    return count; // 返回实际写入的字节数
}

/**
 * @brief 设备mmap回调函数。
 * 允许用户空间将模拟的帧缓冲`virt_mem`映射到其地址空间。
 *
 * @param filp file指针。
 * @param vma vm_area_struct指针，描述了用户空间的虚拟内存区域。
 * @return 成功返回0，失败返回负数错误码。
 */
static int ubuntu_lcd_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long addr;
    // 获取virt_mem的物理地址
    addr = virt_to_phys(virt_mem);

    // 将物理内存页映射到用户空间的虚拟内存区域
    // vma: 虚拟内存区域
    // vma->vm_start: 用户空间起始地址
    // addr >> PAGE_SHIFT: 物理页帧号 (物理地址右移PAGE_SHIFT位)
    // PAGE_SIZE: 映射的长度
    // vma->vm_page_prot: 内存保护标志
    if (io_remap_pfn_range(vma, vma->vm_start, addr >> PAGE_SHIFT,
                           PAGE_SIZE, vma->vm_page_prot))
    {
        printk(KERN_ERR "%s: io_remap_pfn_range failed\n", __func__);
        return -EAGAIN; // 映射失败
    }
    printk(KERN_INFO "ubuntu_lcd: mmap successful. virt_mem physical addr: 0x%lx\n", addr);
    return 0;
}

/**
 * @brief 设备ioctl回调函数。
 * 处理用户空间发送的控制命令。
 *
 * @param filp file指针。
 * @param cmd ioctl命令。
 * @param arg 命令参数。
 * @return 成功返回0，失败返回负数错误码。
 */
static long ubuntu_lcd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int err = 0;
    int ret = 0;
    int ioarg = 0;

    /* 检测命令的有效性 */
    // 检查幻数是否匹配
    if (_IOC_TYPE(cmd) != MEMDEV_IOC_MAGIC)
    {
        printk(KERN_ERR "ubuntu_lcd: ioctl invalid magic number: 0x%x\n", _IOC_TYPE(cmd));
        return -EINVAL;
    }
    // 检查命令编号是否在有效范围内
    if (_IOC_NR(cmd) > MEMDEV_IOC_MAXNR)
    {
        printk(KERN_ERR "ubuntu_lcd: ioctl invalid command number: %d\n", _IOC_NR(cmd));
        return -EINVAL;
    }

    /* 根据命令类型，检测参数空间是否可以访问 */
    // 如果是读操作，检查用户空间地址是否可写 (因为内核要写入用户空间)
    if (_IOC_DIR(cmd) & _IOC_READ)
        err = !access_ok((void __user *)arg, _IOC_SIZE(cmd));
    // 如果是写操作，检查用户空间地址是否可读 (因为内核要从用户空间读取)
    else if (_IOC_DIR(cmd) & _IOC_WRITE)
        err = !access_ok((void __user *)arg, _IOC_SIZE(cmd));

    if (err)
    {
        printk(KERN_ERR "ubuntu_lcd: ioctl access_ok failed for arg: 0x%lx\n", arg);
        return -EFAULT; // 用户空间地址无效
    }

    /* 根据命令，执行相应的操作 */
    switch (cmd)
    {
    /* 打印当前设备信息 */
    case MEMDEV_IOCPRINT:
        printk(KERN_INFO "<--- CMD MEMDEV_IOCPRINT Done--->\n\n");
        break;

    /* 获取参数 */
    case MEMDEV_IOCGETDATA:
        ioarg = 1101; // 内核要返回给用户的值
        ret = __put_user(ioarg, (int __user *)arg); // 将ioarg写入用户空间
        if (ret)
            printk(KERN_ERR "ubuntu_lcd: __put_user failed.\n");
        break;

    /* 设置参数 */
    case MEMDEV_IOCSETDATA:
        ret = __get_user(ioarg, (int __user *)arg); // 从用户空间读取ioarg
        if (ret)
            printk(KERN_ERR "ubuntu_lcd: __get_user failed.\n");
        printk(KERN_INFO "<--- In Kernel MEMDEV_IOCSETDATA ioarg = %d --->\n\n", ioarg);
        break;

    default:
        printk(KERN_ERR "ubuntu_lcd: ioctl unknown command: 0x%x\n", cmd);
        return -EINVAL; // 未知命令
    }
    return ret; // 返回操作结果
}

/**
 * @brief 文件操作结构体。
 * 定义了字符设备支持的各种文件操作函数。
 */
static const struct file_operations fops = {
    .owner = THIS_MODULE,         // 指向拥有此结构的模块，防止模块在被使用时卸载
    .open = ubuntu_lcd_open,      // 设备打开操作
    .release = ubuntu_lcd_release,    // 设备关闭操作
    .read = ubuntu_lcd_read,      // 设备读取操作
    .write = ubuntu_lcd_write,    // 设备写入操作
    .mmap = ubuntu_lcd_mmap,      // 设备mmap操作
    .unlocked_ioctl = ubuntu_lcd_ioctl, // 设备ioctl操作 (非大内核锁版本)
};

/**
 * @brief 模块初始化函数。
 * 当模块加载时调用，负责设备的注册和初始化。
 *
 * @return 成功返回0，失败返回负数错误码。
 */
static int __init ubuntu_lcd_init(void)
{
    int ret;

    // 1. 动态分配字符设备主/次设备号
    // dev: 输出分配到的设备号
    // 0: 次设备号起始值
    // 1: 分配的设备号数量
    // DEVICENAME: 设备名称，会在/proc/devices中显示
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICENAME);
    if (ret)
    {
        printk(KERN_ERR "ubuntu_lcd: register fail\n");
        // 如果分配失败，无需注销，因为尚未成功分配
        return ret;
    }

    // 2. 分配并初始化cdev结构体
    cdev_t = cdev_alloc();
    if (!cdev_t)
    {
        printk(KERN_ERR "ubuntu_lcd: cdev_alloc fail\n");
        unregister_chrdev_region(dev, 1); // 如果cdev分配失败，需要注销已分配的设备号
        return -ENOMEM;
    }

    cdev_t->owner = THIS_MODULE; // 设置cdev的拥有者为当前模块
    cdev_init(cdev_t, &fops);    // 初始化cdev，并关联文件操作结构体fops

    // 3. 将cdev添加到内核，使其与设备号关联
    ret = cdev_add(cdev_t, dev, 1); // 1表示关联一个设备号
    if (ret)
    {
        printk(KERN_ERR "ubuntu_lcd: cdev_add fail\n");
        cdev_del(cdev_t);             // 删除cdev，会释放其内部资源
        unregister_chrdev_region(dev, 1); // 失败时，注销设备号
        return ret;
    }

    // 4. 动态创建设备类，用于在/sys/class目录下创建目录
    cdev_class = class_create(THIS_MODULE, DEVICENAME);
    if (IS_ERR(cdev_class)) // 检查class_create是否成功
    {
        printk(KERN_ERR "ubuntu_lcd: cannot create a cdev_class\n");
        cdev_del(cdev_t);             // 失败时，删除cdev
        unregister_chrdev_region(dev, 1); // 失败时，注销设备号
        return PTR_ERR(cdev_class);   // 返回错误码
    }

    // 5. 动态创建设备节点，用于在/dev目录下创建设备文件
    // cdev_class: 关联的设备类
    // NULL: 父设备 (无)
    // dev: 设备号
    // 0: 次设备号 (此处为0，因为只注册了一个设备)
    // DEVICENAME: 设备文件名 (如/dev/ubuntu_lcd)
    device_create(cdev_class, NULL, dev, NULL, DEVICENAME);

    // 6. 设置默认的LCD宽高像素位深，并计算帧缓冲大小
    vinfo.xres = 800;
    vinfo.yres = 480;
    vinfo.bits_per_pixel = 32;
    page_size2 = vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8; // 计算总字节数

    // 7. 分配模拟的帧缓冲内存
    // page_size2: 分配的字节数
    // GFP_KERNEL: 内核内存分配标志，表示在进程上下文分配，可以睡眠
    virt_mem = kzalloc(page_size2, GFP_KERNEL);
    if (!virt_mem)
    {
        printk(KERN_ERR "ubuntu_lcd: kzalloc for virt_mem failed!\n");
        device_destroy(cdev_class, dev);
        class_destroy(cdev_class);
        cdev_del(cdev_t);
        unregister_chrdev_region(dev, 1);
        return -ENOMEM;
    }

    printk(KERN_INFO "ubuntu_lcd: initialized successfully. Framebuffer size: %u bytes\n", page_size2);
    return 0;
}

/**
 * @brief 模块退出函数。
 * 当模块卸载时调用，负责设备的注销和资源的释放。
 */
static void __exit ubuntu_lcd_cleanup(void)
{
    // 1. 释放模拟的帧缓冲内存
    if (virt_mem)
    {
        kfree(virt_mem);
        printk(KERN_INFO "ubuntu_lcd: virt_mem freed.\n");
    }
    // 2. 删除cdev结构体
    cdev_del(cdev_t);
    // 3. 销毁设备节点
    device_destroy(cdev_class, dev);
    // 4. 销毁设备类
    class_destroy(cdev_class);
    // 5. 注销设备号
    unregister_chrdev_region(dev, 1);

    printk(KERN_INFO "ubuntu_lcd: cleaned up\n");
}

// 注册模块初始化和退出函数
module_init(ubuntu_lcd_init);
module_exit(ubuntu_lcd_cleanup);

// 声明模块许可证为GPL，避免内核污染警告
MODULE_LICENSE("GPL");
